﻿// -----------------------------------------------------------------------
// <copyright file="DataTransferObject.cs" company="JSC Centido">
//  Copyright (c) JSC Centido. All rights reserved.
// </copyright>
// -----------------------------------------------------------------------

using System;
using System.Linq;
using System.Globalization;
using System.Xml;
using System.Xml.Schema;
using System.Xml.Serialization;

namespace Centido.Core.Dto
{
    /// <summary>
    ///   A data transfer object.
    /// </summary>
    public abstract class DataTransferObject : IXmlSerializable
    {
        /// <summary>
        ///   The name of the tag stores the version of the object xml schema.
        /// </summary>
        public const string XmlSchemaVersionTag = "v";


        /// <summary>
        ///   This method is reserved and should not be used. When implementing the IXmlSerializable interface, you should return null (Nothing in Visual Basic) from this method, and instead, if specifying a custom schema is required, apply the <see cref="T:System.Xml.Serialization.XmlSchemaProviderAttribute"/> to the class.
        /// </summary>
        /// <returns>
        ///   An <see cref="T:System.Xml.Schema.XmlSchema"/> that describes the XML representation of the object that is produced by the <see cref="M:System.Xml.Serialization.IXmlSerializable.WriteXml(System.Xml.XmlWriter)"/> method and consumed by the <see cref="M:System.Xml.Serialization.IXmlSerializable.ReadXml(System.Xml.XmlReader)"/> method.
        /// </returns>
        public abstract XmlSchema GetSchema();

        /// <summary>
        ///   Generates an object from its XML representation.
        /// </summary>
        /// <param name="reader">The <see cref="T:System.Xml.XmlReader"/> stream from which the object is deserialized. 
        /// </param>
        public abstract void ReadXml(XmlReader reader);

        /// <summary>
        ///   Converts an object into its XML representation.
        /// </summary>
        /// <param name="writer">The <see cref="T:System.Xml.XmlWriter"/> stream to which the object is serialized. 
        /// </param>
        public abstract void WriteXml(XmlWriter writer);


        /// <summary>
        ///   Reads a string value inside the specified xml attribute.
        /// </summary>
        /// <param name="reader">The xml reader.</param>
        /// <param name="attributeName">The name of the attribute.</param>
        /// <param name="isRequired">True if the attribute is required; otherwise, false.</param>
        /// <exception cref="ArgumentNullException">Thrown when the value of <paramref name="reader"/> is null.</exception>
        /// <exception cref="ArgumentNullException">Thrown when the value of <paramref name="attributeName"/> is null.</exception>
        /// <exception cref="ArgumentException">Thrown when the value of <paramref name="attributeName"/> is an empty string.</exception>
        /// <exception cref="ArgumentException">Thrown when the attribute is required and it was not found.</exception>
        /// <returns>Returns the string value inside the specified attribute.</returns>
        protected static string ReadStringAttribute(XmlReader reader, string attributeName, bool isRequired = true)
        {
            if (reader == null)
                throw new ArgumentNullException("reader");
            if (attributeName == null)
                throw new ArgumentNullException("attributeName");
            if (string.IsNullOrEmpty(attributeName))
                throw new ArgumentException("The attribute name cannot be empty.");

            var value = reader.GetAttribute(attributeName);

            if (string.IsNullOrEmpty(value))
            {
                if (isRequired)
                    throw new ArgumentException(string.Format(
                        "Cannot find the required attribute (node = {0}, attribute = {1})",
                        reader.Name,
                        attributeName));

                return null;
            }

            return value;
        }


        /// <summary>
        ///   Reads a guid value inside the specified xml attribute.
        /// </summary>
        /// <param name="reader">The xml reader.</param>
        /// <param name="attributeName">The name of the attribute.</param>
        /// <param name="isRequired">True if the attribute is required; otherwise, false.</param>
        /// <returns>>Returns the guid value inside the specified attribute.</returns>
        protected static Guid? ReadGuidAttribute(XmlReader reader, string attributeName, bool isRequired = true)
        {
            if (reader == null)
                throw new ArgumentNullException("reader");
            if (attributeName == null)
                throw new ArgumentNullException("attributeName");
            if (string.IsNullOrEmpty(attributeName))
                throw new ArgumentException("The attribute name cannot be empty.");

            var valueStr = reader.GetAttribute(attributeName);

            if (string.IsNullOrEmpty(valueStr))
            {
                if (isRequired)
                    throw new ArgumentException(string.Format(
                        "Cannot find the required attribute (node = {0}, attribute = {1})",
                        reader.Name,
                        attributeName));

                return null;
            }

            try
            {
                return new Guid(valueStr);
            }
            catch (FormatException)
            {
                throw new ArgumentException(string.Format(
                        "Cannot read an guid attribute (node = {0}, attribute = {1}, value = {2})",
                        reader.Name,
                        attributeName,
                        valueStr));
            }
        }


        /// <summary>
        ///   Reads an integer value inside the specified xml attribute.
        /// </summary>
        /// <param name="reader">The xml reader.</param>
        /// <param name="attributeName">The name of the attribute.</param>
        /// <param name="isRequired">True if the attribute is required; otherwise, false.</param>
        /// <exception cref="ArgumentNullException">Thrown when the value of <paramref name="reader"/> is null.</exception>
        /// <exception cref="ArgumentNullException">Thrown when the value of <paramref name="attributeName"/> is null.</exception>
        /// <exception cref="ArgumentException">Thrown when the value of <paramref name="attributeName"/> is an empty string.</exception>
        /// <exception cref="ArgumentException">Thrown when the attribute is required and it was not found.</exception>
        /// <exception cref="ArgumentException">Thrown when the attribute value is not an integer value.</exception>
        /// <returns>Returns the integer value inside the specified attribute.</returns>
        protected static int? ReadIntAttribute(XmlReader reader, string attributeName, bool isRequired = true)
        {
            if (reader == null)
                throw new ArgumentNullException("reader");
            if (attributeName == null) 
                throw new ArgumentNullException("attributeName");
            if (string.IsNullOrEmpty(attributeName))
                throw new ArgumentException("The attribute name cannot be empty.");

            var valueStr = reader.GetAttribute(attributeName);

            if (string.IsNullOrEmpty(valueStr))
            {
                if (isRequired)
                    throw new ArgumentException(string.Format(
                        "Cannot find the required attribute (node = {0}, attribute = {1})",
                        reader.Name,
                        attributeName));

                return null;
            }

            int value;

            if (!int.TryParse(valueStr, out value))
                throw new ArgumentException(string.Format(
                    "Cannot read an integer attribute (node = {0}, attribute = {1}, value = {2})",
                    reader.Name,
                    attributeName,
                    valueStr));

            return value;
        }


        /// <summary>
        ///   Reads a small integer value inside the specified xml attribute.
        /// </summary>
        /// <param name="reader">The xml reader.</param>
        /// <param name="attributeName">The name of the attribute.</param>
        /// <param name="isRequired">True if the attribute is required; otherwise, false.</param>
        /// <exception cref="ArgumentNullException">Thrown when the value of <paramref name="reader"/> is null.</exception>
        /// <exception cref="ArgumentNullException">Thrown when the value of <paramref name="attributeName"/> is null.</exception>
        /// <exception cref="ArgumentException">Thrown when the value of <paramref name="attributeName"/> is an empty string.</exception>
        /// <exception cref="ArgumentException">Thrown when the attribute is required and it was not found.</exception>
        /// <exception cref="ArgumentException">Thrown when the attribute value is not a small integer value.</exception>
        /// <returns>Returns the integer value inside the specified attribute.</returns>
        protected static short? ReadSmallIntAttribute(XmlReader reader, string attributeName, bool isRequired = true)
        {
            if (reader == null)
                throw new ArgumentNullException("reader");
            if (attributeName == null)
                throw new ArgumentNullException("attributeName");
            if (string.IsNullOrEmpty(attributeName))
                throw new ArgumentException("The attribute name cannot be empty.");

            var valueStr = reader.GetAttribute(attributeName);

            if (string.IsNullOrEmpty(valueStr))
            {
                if (isRequired)
                    throw new ArgumentException(string.Format(
                        "Cannot find the required attribute (node = {0}, attribute = {1})",
                        reader.Name,
                        attributeName));

                return null;
            }

            short value;

            if (!short.TryParse(valueStr, out value))
                throw new ArgumentException(string.Format(
                    "Cannot read a small integer attribute (node = {0}, attribute = {1}, value = {2})",
                    reader.Name,
                    attributeName,
                    valueStr));

            return value;
        }


        /// <summary>
        ///   Reads an big integer value inside the specified xml attribute.
        /// </summary>
        /// <param name="reader">The xml reader.</param>
        /// <param name="attributeName">The name of the attribute.</param>
        /// <param name="isRequired">True if the attribute is required; otherwise, false.</param>
        /// <exception cref="ArgumentNullException">Thrown when the value of <paramref name="reader"/> is null.</exception>
        /// <exception cref="ArgumentNullException">Thrown when the value of <paramref name="attributeName"/> is null.</exception>
        /// <exception cref="ArgumentException">Thrown when the value of <paramref name="attributeName"/> is an empty string.</exception>
        /// <exception cref="ArgumentException">Thrown when the attribute is required and it was not found.</exception>
        /// <exception cref="ArgumentException">Thrown when the attribute value is not an integer value.</exception>
        /// <returns>Returns the integer value inside the specified attribute.</returns>
        protected static long? ReadBigIntAttribute(XmlReader reader, string attributeName, bool isRequired = true)
        {
            if (reader == null)
                throw new ArgumentNullException("reader");
            if (attributeName == null)
                throw new ArgumentNullException("attributeName");
            if (string.IsNullOrEmpty(attributeName))
                throw new ArgumentException("The attribute name cannot be empty.");

            var valueStr = reader.GetAttribute(attributeName);

            if (string.IsNullOrEmpty(valueStr))
            {
                if (isRequired)
                    throw new ArgumentException(string.Format(
                        "Cannot find the required attribute (node = {0}, attribute = {1})",
                        reader.Name,
                        attributeName));

                return null;
            }

            long value;

            if (!long.TryParse(valueStr, out value))
                throw new ArgumentException(string.Format(
                    "Cannot read a big integer attribute (node = {0}, attribute = {1}, value = {2})",
                    reader.Name,
                    attributeName,
                    valueStr));

            return value;
        }


        /// <summary>
        ///   Reads an decimal value inside the specified xml attribute.
        /// </summary>
        /// <param name="reader">The xml reader.</param>
        /// <param name="attributeName">The name of the attribute.</param>
        /// <param name="isRequired">True if the attribute is required; otherwise, false.</param>
        /// <exception cref="ArgumentNullException">Thrown when the value of <paramref name="reader"/> is null.</exception>
        /// <exception cref="ArgumentNullException">Thrown when the value of <paramref name="attributeName"/> is null.</exception>
        /// <exception cref="ArgumentException">Thrown when the value of <paramref name="attributeName"/> is an empty string.</exception>
        /// <exception cref="ArgumentException">Thrown when the attribute is required and it was not found.</exception>
        /// <exception cref="ArgumentException">Thrown when the attribute value is not a decimal value.</exception>
        /// <returns>Returns the decimal value inside the specified attribute.</returns>
        protected static decimal? ReadDecimalAttribute(XmlReader reader, string attributeName, bool isRequired = true)
        {
            if (reader == null)
                throw new ArgumentNullException("reader");
            if (attributeName == null)
                throw new ArgumentNullException("attributeName");
            if (string.IsNullOrEmpty(attributeName))
                throw new ArgumentException("The attribute name cannot be empty.");

            var valueStr = reader.GetAttribute(attributeName);

            if (string.IsNullOrEmpty(valueStr))
            {
                if (isRequired)
                    throw new ArgumentException(string.Format(
                        "Cannot find the required attribute (node = {0}, attribute = {1})",
                        reader.Name,
                        attributeName));

                return null;
            }

            decimal value;

            try
            {
                value = decimal.Parse(valueStr, CultureInfo.InvariantCulture);
            }
            catch (FormatException)
            {
                throw new ArgumentException(string.Format(
                    "Cannot read an decimal attribute (node = {0}, attribute = {1}, value = {2})",
                    reader.Name,
                    attributeName,
                    valueStr));
            }

            return value;
        }


        /// <summary>
        ///   Reads an boolean value inside the specified xml attribute.
        /// </summary>
        /// <param name="reader">The xml reader.</param>
        /// <param name="attributeName">The name of the attribute.</param>
        /// <param name="isRequired">True if the attribute is required; otherwise, false.</param>
        /// <exception cref="ArgumentNullException">Thrown when the value of <paramref name="reader"/> is null.</exception>
        /// <exception cref="ArgumentNullException">Thrown when the value of <paramref name="attributeName"/> is null.</exception>
        /// <exception cref="ArgumentException">Thrown when the value of <paramref name="attributeName"/> is an empty string.</exception>
        /// <exception cref="ArgumentException">Thrown when the attribute is required and it was not found.</exception>
        /// <exception cref="ArgumentException">Thrown when the attribute value is not an boolean value.</exception>
        /// <returns>Returns the boolean value inside the specified attribute.</returns>
        protected static bool? ReadBoolAttribute(XmlReader reader, string attributeName, bool isRequired = true)
        {
            if (reader == null)
                throw new ArgumentNullException("reader");
            if (attributeName == null)
                throw new ArgumentNullException("attributeName");
            if (string.IsNullOrEmpty(attributeName))
                throw new ArgumentException("The attribute name cannot be empty.");

            var valueStr = reader.GetAttribute(attributeName);

            if (string.IsNullOrEmpty(valueStr))
            {
                if (isRequired)
                    throw new ArgumentException(string.Format(
                        "Cannot find the required attribute (node = {0}, attribute = {1})",
                        reader.Name,
                        attributeName));

                return null;
            }

            bool value;

            if (!bool.TryParse(valueStr, out value))
                throw new ArgumentException(string.Format(
                    "Cannot read an boolean attribute (node = {0}, attribute = {1}, value = {2})",
                    reader.Name,
                    attributeName,
                    valueStr));

            return value;
        }


        /// <summary>
        ///   Reads an datetime value inside the specified xml attribute.
        /// </summary>
        /// <param name="reader">The xml reader.</param>
        /// <param name="attributeName">The name of the attribute.</param>
        /// <param name="isRequired">True if the attribute is required; otherwise, false.</param>
        /// <exception cref="ArgumentNullException">Thrown when the value of <paramref name="reader"/> is null.</exception>
        /// <exception cref="ArgumentNullException">Thrown when the value of <paramref name="attributeName"/> is null.</exception>
        /// <exception cref="ArgumentException">Thrown when the value of <paramref name="attributeName"/> is an empty string.</exception>
        /// <exception cref="ArgumentException">Thrown when the attribute is required and it was not found.</exception>
        /// <exception cref="ArgumentException">Thrown when the attribute value is not an datetime value.</exception>
        /// <returns>Returns the datetime value inside the specified attribute.</returns>
        protected static DateTime? ReadDateTimeAttribute(XmlReader reader, string attributeName, bool isRequired = true)
        {
            if (reader == null)
                throw new ArgumentNullException("reader");
            if (attributeName == null)
                throw new ArgumentNullException("attributeName");
            if (string.IsNullOrEmpty(attributeName))
                throw new ArgumentException("The attribute name cannot be empty.");

            var valueStr = reader.GetAttribute(attributeName);

            if (string.IsNullOrEmpty(valueStr))
            {
                if (isRequired)
                    throw new ArgumentException(string.Format(
                        "Cannot find the required attribute (node = {0}, attribute = {1})",
                        reader.Name,
                        attributeName));

                return null;
            }

            DateTime value;

            try
            {
                value = DateTime.Parse(valueStr, CultureInfo.InvariantCulture);
            }
            catch (FormatException)
            {
                throw new ArgumentException(string.Format(
                    "Cannot read an datetime attribute (node = {0}, attribute = {1}, value = {2})",
                    reader.Name,
                    attributeName,
                    valueStr));
            }

            return value;
        }


        /// <summary>
        ///   Reads an timespan value inside the specified xml attribute.
        /// </summary>
        /// <param name="reader">The xml reader.</param>
        /// <param name="attributeName">The name of the attribute.</param>
        /// <param name="isRequired">True if the attribute is required; otherwise, false.</param>
        /// <exception cref="ArgumentNullException">Thrown when the value of <paramref name="reader"/> is null.</exception>
        /// <exception cref="ArgumentNullException">Thrown when the value of <paramref name="attributeName"/> is null.</exception>
        /// <exception cref="ArgumentException">Thrown when the value of <paramref name="attributeName"/> is an empty string.</exception>
        /// <exception cref="ArgumentException">Thrown when the attribute is required and it was not found.</exception>
        /// <exception cref="ArgumentException">Thrown when the attribute value is not an timespan value.</exception>
        /// <returns>Returns the timespan value inside the specified attribute.</returns>
        protected static TimeSpan? ReadTimeSpanAttribute(XmlReader reader, string attributeName, bool isRequired = true)
        {
            if (reader == null)
                throw new ArgumentNullException("reader");
            if (attributeName == null)
                throw new ArgumentNullException("attributeName");
            if (string.IsNullOrEmpty(attributeName))
                throw new ArgumentException("The attribute name cannot be empty.");

            var valueStr = reader.GetAttribute(attributeName);

            if (string.IsNullOrEmpty(valueStr))
            {
                if (isRequired)
                    throw new ArgumentException(string.Format(
                        "Cannot find the required attribute (node = {0}, attribute = {1})",
                        reader.Name,
                        attributeName));

                return null;
            }

            TimeSpan value;

            if (!TimeSpan.TryParse(valueStr, out value))
                throw new ArgumentException(string.Format(
                    "Cannot read an timespan attribute (node = {0}, attribute = {1}, value = {2})",
                    reader.Name,
                    attributeName,
                    valueStr));

            return value;
        }


        /// <summary>
        ///   Reads an enumaration value inside the specified xml attribute.
        /// </summary>
        /// <param name="reader">The xml reader.</param>
        /// <param name="attributeName">The name of the attribute.</param>
        /// <param name="enumType">The enumeration type.</param>
        /// <param name="isRequired">True if the attribute is required; otherwise, false.</param>
        /// <exception cref="ArgumentNullException">Thrown when the value of <paramref name="reader"/> is null.</exception>
        /// <exception cref="ArgumentNullException">Thrown when the value of <paramref name="attributeName"/> is null.</exception>
        /// <exception cref="ArgumentNullException">Thrown when the value of <paramref name="enumType"/> is null.</exception>
        /// <exception cref="ArgumentException">Thrown when the value of <paramref name="attributeName"/> is an empty string.</exception>
        /// <exception cref="ArgumentException">Thrown when the attribute is required and it was not found.</exception>
        /// <exception cref="ArgumentException">Thrown when the attribute value is not an integer value.</exception>
        /// <exception cref="ArgumentException">Thrown when the attribute value does not belong to the enumeration.</exception>
        /// <returns>Returns the timespan base enumeration value inside the specified attribute.</returns>
        protected static int? ReadEnumAttribute(XmlReader reader, string attributeName,
            Type enumType, bool isRequired = true)
        {
            if (reader == null)
                throw new ArgumentNullException("reader");
            if (attributeName == null)
                throw new ArgumentNullException("attributeName");
            if (string.IsNullOrEmpty(attributeName))
                throw new ArgumentException("The attribute name cannot be empty.");
            if (enumType == null)
                throw new ArgumentNullException("enumType");

            var valueStr = reader.GetAttribute(attributeName);

            if (string.IsNullOrEmpty(valueStr))
            {
                if (isRequired)
                    throw new ArgumentException(string.Format(
                        "Cannot find the required attribute (node = {0}, attribute = {1})",
                        reader.Name,
                        attributeName));

                return null;
            }

            int value;

            if (!int.TryParse(valueStr, out value))
                throw new ArgumentException(string.Format(
                    "Cannot read an integer attribute (node = {0}, attribute = {1}, value = {2})",
                    reader.Name,
                    attributeName,
                    valueStr));

            if (!Enum.GetValues(enumType).Cast<int>().Contains(value))
                throw new ArgumentException(string.Format(
                    "The attribute value does not belong to the enumeration (node = {0}, attribute = {1}, value = {2})",
                    reader.Name,
                    attributeName,
                    valueStr));

            return value;
        }
    }
}
